<html>
 <head>
  <meta charset="UTF-8">
 </head>
 <body>
  <h1 data-lake-id="Ubunj" id="Ubunj"><span data-lake-id="u499d3c5f" id="u499d3c5f">典型回答</span></h1>
  <p data-lake-id="u5a595101" id="u5a595101"><br></p>
  <p data-lake-id="uab49b663" id="uab49b663"><span data-lake-id="ud13952e0" id="ud13952e0">这是一个比较典型的关于Redis分布式锁的综合性问题，其实考察的就是关于分布式锁的熟悉程度，尤其是Redis的分布式锁的熟悉程度。</span></p>
  <p data-lake-id="u7b90dc04" id="u7b90dc04"><span data-lake-id="u955fa475" id="u955fa475">​</span><br></p>
  <p data-lake-id="u749241d6" id="u749241d6"><span data-lake-id="u4c292542" id="u4c292542">我总结了一下关于这个问题可以回答的几个方向和关键点。</span></p>
  <p data-lake-id="uf83cb537" id="uf83cb537"><span data-lake-id="u958e2a38" id="u958e2a38">​</span><br></p>
  <h3 data-lake-id="TZqyW" id="TZqyW"><span data-lake-id="u61b45b78" id="u61b45b78">锁的基本要求</span></h3>
  <p data-lake-id="u390874df" id="u390874df"><span data-lake-id="u1e3f30e5" id="u1e3f30e5">​</span><br></p>
  <p data-lake-id="uf7ea6a59" id="uf7ea6a59"><span data-lake-id="u3a4e4748" id="u3a4e4748">一个分布式锁有很多基本要求，比如说锁的</span><strong><span data-lake-id="u3ff2e63e" id="u3ff2e63e">互斥性</span></strong><span data-lake-id="ube4739a2" id="ube4739a2">、</span><strong><span data-lake-id="ue4b70b01" id="ue4b70b01">可重入性</span></strong><span data-lake-id="u741b0bf7" id="u741b0bf7">、</span><strong><span data-lake-id="u885c8a5d" id="u885c8a5d">锁的性能</span></strong><span data-lake-id="ufd088eb1" id="ufd088eb1">等问题。</span></p>
  <p data-lake-id="u1ac8ce1a" id="u1ac8ce1a"><span data-lake-id="ue74527e9" id="ue74527e9">​</span><br></p>
  <p data-lake-id="u0caac605" id="u0caac605"><span data-lake-id="u00266db6" id="u00266db6">对于锁的互斥性，可以借助setnx来保证，因为这个操作本身就是一个原子性操作，并且结合Redis的单线程的机制，就可以保证互斥性。</span></p>
  <p data-lake-id="uc113a584" id="uc113a584"><span data-lake-id="u6a4ef898" id="u6a4ef898">​</span><br></p>
  <p data-lake-id="uf212e67c" id="uf212e67c"><br></p>
  <p data-lake-id="ubec9b347" id="ubec9b347"><span data-lake-id="u07c8bea1" id="u07c8bea1">因为Redis是基于内存的，所以他的性能也是很高的，这个就没啥好说的了，大家都知道的。</span></p>
  <p data-lake-id="ucad73e74" id="ucad73e74"><span data-lake-id="ua492fd29" id="ua492fd29">​</span><br></p>
  <p data-lake-id="u96940532" id="u96940532"><br></p>
  <p data-lake-id="u05630704" id="u05630704"><span data-lake-id="uf31e8c4e" id="uf31e8c4e">至于可重入性，其实就是说一个线程，在锁没有释放的情况下，他是可以反复的拿到同一把锁的。并且需要在锁种记录加锁次数，用来保证重入几次就需要解锁几次。用setnx也是可以实现的。</span></p>
  <p data-lake-id="u6cf85f68" id="u6cf85f68"><span data-lake-id="ud4ddd986" id="ud4ddd986">​</span><br></p>
  <p data-lake-id="u17a607fb" id="u17a607fb"><br></p>
  <p data-lake-id="u91a94d2f" id="u91a94d2f"><br></p>
  <p data-lake-id="u8f1e4f61" id="u8f1e4f61"><span data-lake-id="ufe2e10ed" id="ufe2e10ed">当然，如果我们直接使用Redisson的话，他是支持可重入锁的实现的。可以直接用。</span></p>
  <p data-lake-id="u86b118d9" id="u86b118d9"><span data-lake-id="ub0dd58c7" id="ub0dd58c7">​</span><br></p>
  <p data-lake-id="u55bc2753" id="u55bc2753"><br></p>
  <h3 data-lake-id="qjghj" id="qjghj"><span data-lake-id="uc9757285" id="uc9757285">误解锁问题</span></h3>
  <p data-lake-id="u69e902fc" id="u69e902fc"><br></p>
  <p data-lake-id="ud95cd9c5" id="ud95cd9c5"><span data-lake-id="ua5e0b18f" id="ua5e0b18f">其实就是要确保只有锁的持有者能释放锁，避免其他客户端误解锁。这个问题其实挺傻的，但是我们实际就发生过，因为有的时候我们是在finally中去释放锁，finally有一定会执行，那么就可能会导致虽然没拿到锁，但是当他执行finally的时候，也可能把锁给解了。</span></p>
  <p data-lake-id="ud618c777" id="ud618c777"><span data-lake-id="uc4a3a95f" id="uc4a3a95f">​</span><br></p>
  <p data-lake-id="ua365a4a1" id="ua365a4a1"><span data-lake-id="u27d766cb" id="u27d766cb">所以，需要解决这个问题，我们就需要在使用setnx加锁时把具体持有锁的owner放进去，和上面一样，线程ID也好，业务单号也好，总之需要做一下判断。</span></p>
  <p data-lake-id="u617d3e30" id="u617d3e30"><span data-lake-id="u2a12162c" id="u2a12162c">​</span><br></p>
  <p data-lake-id="ub0f78bca" id="ub0f78bca"><span data-lake-id="udf153031" id="udf153031">如果用了Redisson是不存在这个问题的。</span></p>
  <p data-lake-id="udba7f1ba" id="udba7f1ba"><br></p>
  <h3 data-lake-id="a5zWT" id="a5zWT"><span data-lake-id="u7371629b" id="u7371629b">锁的有效时间</span></h3>
  <p data-lake-id="uab402057" id="uab402057"><br></p>
  <p data-lake-id="u2573e869" id="u2573e869"><span data-lake-id="u474e9c42" id="u474e9c42">为了避免死锁，我们一般会给一个分布式锁设置一个超时时间，如上面我们用的setnx的方案，其实就是设置了一个超时时间的。</span></p>
  <p data-lake-id="ud945e663" id="ud945e663"><span data-lake-id="u87c8817b" id="u87c8817b">​</span><br></p>
  <p data-lake-id="u42826587" id="u42826587"><span data-lake-id="u0325f191" id="u0325f191">但是有的是，代码如果执行的比较慢的话，比如设置的超时时间是3秒，但是代码执行了5秒，那么就会导致在第三秒的时候，key超时了就自动解锁了，那么其他的线程就可以拿到锁了，这时候就会发生并发的问题了。</span></p>
  <p data-lake-id="u25d64b91" id="u25d64b91"><span data-lake-id="ue403bcb7" id="ue403bcb7">​</span><br></p>
  <p data-lake-id="u88d0b87e" id="u88d0b87e"><span data-lake-id="u91611b2a" id="u91611b2a">所以，我们需要有一个好的办法来解决。一种是设置一个更长的超时时间，避免提前释放，我见过有人把分布式锁设置半个小时。。。</span></p>
  <p data-lake-id="u08e2165e" id="u08e2165e"><span data-lake-id="u2fe4370b" id="u2fe4370b">​</span><br></p>
  <p data-lake-id="u3ceb8888" id="u3ceb8888"><span data-lake-id="u7d99b8ec" id="u7d99b8ec">但是这个方案非常不好，因为分布式锁是影响并发的，锁的时间长，意味着加锁时间段内只能有一个线程操作，那么并发度就会大大降低。</span></p>
  <p data-lake-id="uf934f507" id="uf934f507"><span data-lake-id="u2e927281" id="u2e927281">​</span><br></p>
  <p data-lake-id="u67fbd4ae" id="u67fbd4ae"><span data-lake-id="u229d5c71" id="u229d5c71">还有一个好的办法，就是像redisson一样，实现一个watch dog的机制，给锁自动做续期，让锁不会提前释放。</span></p>
  <p data-lake-id="u7fe1eaf5" id="u7fe1eaf5"><br></p>
  <p data-lake-id="u1791ce54" id="u1791ce54"><br></p>
  <p data-lake-id="u8624deec" id="u8624deec"><span data-lake-id="ud0e7fcb5" id="ud0e7fcb5">但是需要注意的是，只有我们没有自己主动设置锁的超时时间的时候，watchdog才会续期，如果自己设置了超时时间，那么就不会给你续期了。具体看上面这个原理解读。</span></p>
  <p data-lake-id="ua9893f67" id="ua9893f67"><br></p>
  <h3 data-lake-id="AecZp" id="AecZp"><span data-lake-id="u98e63926" id="u98e63926">单点故障问题</span></h3>
  <p data-lake-id="u569fff3b" id="u569fff3b"><br></p>
  <p data-lake-id="u678a7024" id="u678a7024"><span data-lake-id="u6cdf7bd5" id="u6cdf7bd5">有了自动续期之后，锁就一定可考了吗？其实也不是，这里会存在两个单点问题。</span></p>
  <p data-lake-id="u6788be96" id="u6788be96"><br></p>
  <p data-lake-id="u39ae8feb" id="u39ae8feb"><span data-lake-id="ue104d779" id="ue104d779">首先，在使用单节点Redis实现分布式锁时，如果这个Redis实例挂掉，那么所有使用这个实例的客户端都会出现无法获取锁的情况。</span></p>
  <p data-lake-id="u10c77ba4" id="u10c77ba4"><span data-lake-id="u8789d594" id="u8789d594">​</span><br></p>
  <p data-lake-id="uc3743cb8" id="uc3743cb8"><span data-lake-id="u3ed036e5" id="u3ed036e5">这个问题是有解的，就是引入集群模式，通过哨兵检测redis实例挂掉的情况，提升整个集群的可用性。</span></p>
  <p data-lake-id="ud742cd11" id="ud742cd11"><span data-lake-id="u9ca02714" id="u9ca02714">​</span><br></p>
  <p data-lake-id="u23b3876f" id="u23b3876f"><br></p>
  <p data-lake-id="u3d4c79fc" id="u3d4c79fc"><span data-lake-id="u6bb9c1a3" id="u6bb9c1a3">但是，这个方案同样存在一个单点故障带来的问题：</span></p>
  <p data-lake-id="u8c689ea9" id="u8c689ea9"><span data-lake-id="uf1cf599e" id="uf1cf599e">​</span><br></p>
  <p data-lake-id="ucb277a67" id="ucb277a67"><span data-lake-id="u1b7ce6a9" id="u1b7ce6a9">当使用集群模式部署的时候，如果master一个客户端在master节点加锁成功了，然后没来得及同步数据到其他节点上，他就挂了， 那么这时候如果选出一个新的节点，再有客户端来加锁的时候，就也能加锁成功，因为数据没来得及同步，新的master会认为这个key是不存在的。</span></p>
  <p data-lake-id="uf6433146" id="uf6433146"><span data-lake-id="u0970be52" id="u0970be52">​</span><br></p>
  <p data-lake-id="uddae241a" id="uddae241a"><span data-lake-id="u7a734955" id="u7a734955">为了解决这个问题，redis的作者提出了一个算法——RedLock，他通过这种算法来保证在半数以上加锁成功才认为成功，这样就可以确保即使master挂了，新选出来的master也会有之前的加锁数据。</span></p>
  <p data-lake-id="u261a2252" id="u261a2252"><span data-lake-id="u57c8b0a3" id="u57c8b0a3">​</span><br></p>
  <p data-lake-id="u05bd52cf" id="u05bd52cf"><span data-lake-id="uddf73c86" id="uddf73c86">具体原理见：</span></p>
  <p data-lake-id="u5d111011" id="u5d111011"><span data-lake-id="u4896d6b5" id="u4896d6b5">​</span><br></p>
  <p data-lake-id="uce27447b" id="uce27447b"><br></p>
  <p data-lake-id="ub1e5fd3c" id="ub1e5fd3c"><br></p>
  <h3 data-lake-id="zf6Ba" id="zf6Ba"><span data-lake-id="ub99d1139" id="ub99d1139">网络分区问题</span></h3>
  <p data-lake-id="uec2b5ada" id="uec2b5ada"><br></p>
  <p data-lake-id="u07d0345a" id="u07d0345a"><span data-lake-id="u56b3abae" id="u56b3abae">但是，引入红锁就万事大吉了么。也并不是。红锁同样存在问题。首先就是一个网络分区的问题。</span></p>
  <p data-lake-id="u8a5858c6" id="u8a5858c6"><span data-lake-id="ue13a96fa" id="ue13a96fa">​</span><br></p>
  <p data-lake-id="u86c0f125" id="u86c0f125"><span data-lake-id="u93cc580a" id="u93cc580a">在网络分区的情况下，比如集群发生了脑裂，不同的节点可能会获取到相同的锁，这会导致分布式系统的不一致性问题。</span></p>
  <p data-lake-id="u95fa91e8" id="u95fa91e8"><span data-lake-id="ufd6b83b7" id="ufd6b83b7">​</span><br></p>
  <p data-lake-id="u8149b31b" id="u8149b31b"><br></p>
  <p data-lake-id="ue3326689" id="ue3326689"><span data-lake-id="uc17f0089" id="uc17f0089">但是我们需要注意的是，这个情况虽然会存在节点获取到相同锁，但这种情况只会发生在网络分区发生时，且只会发生在一小部分节点上。而在网络分区恢复后，RedLock 会自动解锁。所以理论上来说是有这个风险，但是实际上来说发生的概率极低。</span></p>
  <p data-lake-id="u8df0dab4" id="u8df0dab4"><span data-lake-id="u9305d33b" id="u9305d33b">​</span><br></p>
  <h3 data-lake-id="OxZ7q" id="OxZ7q"><span data-lake-id="u972ce951" id="u972ce951">时间漂移问题</span></h3>
  <p data-lake-id="uce7a1c76" id="uce7a1c76"><br></p>
  <p data-lake-id="udb4ae5a4" id="udb4ae5a4"><span data-lake-id="u32ac34f6" id="u32ac34f6">除了脑裂，还有一个时钟飘逸的问题，由于不同的机器之间的时间可能存在微小的漂移，这会导致锁的失效时间不一致，也会导致分布式系统的不一致性问题。</span></p>
  <p data-lake-id="u1e6df51f" id="u1e6df51f"><span data-lake-id="ub76c64df" id="ub76c64df">​</span><br></p>
  <p data-lake-id="u5e428ecd" id="u5e428ecd"><span data-lake-id="u84da0ee8" id="u84da0ee8">那么就会导致有的redis实例已经解锁了，那么就会使得新的客户端可以拿到锁。</span></p>
  <p data-lake-id="ubbf1db8b" id="ubbf1db8b"><span data-lake-id="uc58b2459" id="uc58b2459">​</span><br></p>
  <p data-lake-id="u5337729e" id="u5337729e"><span data-lake-id="ua6de3120" id="ua6de3120">这个问题的解决方案是，RedLock 可以使用 NTP 等工具来同步不同机器之间的时间，从而避免时间漂移导致的问题。</span></p>
 </body>
</html>